import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import javax.swing.JFileChooser;
import javax.swing.filechooser.FileNameExtensionFilter;

public class View extends Canvas implements MouseListener,  MouseMotionListener, KeyListener{
	
	/**
	 * class view members
	 */
	private static final long serialVersionUID = 1L;
	private int originX;
	private int originY;
	private double direction;
	private double sizeW;
	private double sizeH;
	private int resolutionW;
	private int resolutionH;

	//original
	private int pointsCount;

	private HashMap<Integer, Point> points = new HashMap<Integer, Point>();
	
	private int edgesCount;
	private ArrayList<Edge> edges = new ArrayList<Edge>();
	
	private int centerX;
	private int centerY;
	private Point centerP;

	
	//for the devision of the screen to 9 parts
	private int partXmin;
	private int partXmax;
	private int partYmin;
	private int partYmax;
	
	private static Operation currentOperation;
	
	private Point lastPoint = null;
	private  Matrix matrix = null;
	private  boolean isClipping;
	

	/**
	 * constructor
	 * initialization of the Hash sets
	 */
	public View(){
		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);
		this.addComponentListener(new java.awt.event.ComponentAdapter() 
		{
		     public void componentResized(ComponentEvent e)
		     {
		    	 resolutionW=((Canvas)e.getSource()).getWidth()-40;
		    	 resolutionH=((Canvas)e.getSource()).getHeight()-40;
		    	 centerX = (resolutionW+40)/2;
		 		 centerY = (resolutionH+40)/2;
		 		 
		 		centerP = new Point(centerX,centerY);
		 		partXmin = (int)((resolutionW+40 )/3.0);
				partXmax = (2*partXmin);
				partYmin =(int) ((resolutionH+40)/3.0);
				//partYmin = (resolutionH+60)/3;
				partYmax = (2*partYmin);
				
		      }
		});
		isClipping = false;
		matrix = new Matrix(3,3);
	}
	
	/**
	 * Read the view file
	 * @param fileName - String representing the file name
	 * @throws IOException
	 */
	public void readView(String fileName) throws IOException{
		BufferedReader in = new BufferedReader(new FileReader(fileName));
		// Read origin
		String line = in.readLine();
		String [] words = line.split(" ");
		originX = Integer.parseInt(words[1]);
		originY = Integer.parseInt(words[2]);
		
		//read direction
		line = in.readLine();
		words = line.split(" ");
		direction = Math.toRadians((-1)*Integer.parseInt(words[1]));
		
		//Read size
		line = in.readLine();
		words = line.split(" ");
		sizeW = Double.parseDouble(words[1]);
		sizeH = Double.parseDouble(words[2]);
		
		//Read resolution
		line = in.readLine();
		words = line.split(" ");
		resolutionW = Integer.parseInt(words[1]);
		resolutionH = Integer.parseInt(words[2]);
		this.setSize(resolutionW+40, resolutionH+40);
		//this.setSize(resolutionW+40, resolutionH+60);
		
		centerX = (resolutionW+40)/2;
		centerY = (resolutionH+40)/2;
		//centerY = (resolutionH+60)/2;
		centerP = new Point(centerX,centerY);
		
		partXmin = (resolutionW+40 )/3;
		partXmax = (2*partXmin);
		partYmin = (resolutionH+40)/3;
		//partYmin = (resolutionH+60)/3;
		partYmax = (2*partYmin);
	}
	
	/**
	 * setters of class members
	 */
	public void setPoints(HashMap<Integer, Point> points) {
		this.points = points;
	}
	public void setPointsCount(int pointsCount) {
		this.pointsCount = pointsCount;
	}
	public void setEdgesCount(int edgesCount) {
		this.edgesCount = edgesCount;
	}
	public void setEdges(ArrayList<Edge> edges) {
		this.edges = edges;
	}
	
	/**
	 * Set the world objects
	 * @param world 
	 */
	public void setArgs(World world){
		setPoints(world.getPoints());
		setPointsCount(world.getPointsCount());
		setEdges(world.getEdges());
		setEdgesCount(world.getEdgesCount());
		
	}
	
	/**
	 * Update View according to the view parameters
	 */
	public void updateView(){
		
		// Move origin to (0,0).
		int moveX,moveY;
		moveX = -1*originX;
		
		moveY = -1*originY;
		
		// Update the points after moving and rotation.
		for (int i=1; i<=pointsCount; i++){
			Integer idx = new Integer(i);
			Point p = points.remove(idx);
			p.translate(moveX, moveY);
			// Rotate.
			points.put(idx, rotate(p,direction));
		}
		// Update edges according to the new points.
		ArrayList<Edge> tmpEdges = new ArrayList<Edge>();
		while (!edges.isEmpty()) {
			Edge e = edges.remove(0);
			int idx1 = e.getIdx1();
			int idx2 = e.getIdx2();
			
			Point p1 = points.get(new Integer(idx1));
			Point p2 = points.get(new Integer(idx2));
			
			tmpEdges.add(new Edge(p1,p2,idx1,idx2));
		}
		
		edges = new ArrayList<Edge>(tmpEdges);
		edgesCount = edges.size();

		// Now we have our constant view = points and edges.
		// Normalize points to fit the resolution window.
		HashMap<Integer, Point>tmpPoints = new HashMap<Integer, Point>();
		for (Integer i : points.keySet())
		{
			double x = points.get(i).x;
			double y = points.get(i).y;
			x /= sizeW;
			y /= sizeH;
			x *= resolutionW;
			y *= resolutionH;
			Point pp = new Point((int)x , (int)y);
			tmpPoints.put(i, pp);
		}
		
		points = new HashMap<Integer, Point>(tmpPoints);
		// Update edges after normalizing.
		for (int i=0;i<edgesCount;i++)
		{
			Integer i1 = edges.get(i).getIdx1();
			Integer i2 = edges.get(i).getIdx2();
			Point pp = points.get(i1);
			edges.get(i).setP1(pp);
			pp = points.get(i2);
			edges.get(i).setP2(pp);
		}
		
	}
	
	/**
	 * Rotate vertex. 
	 * @param p	- points coordinates
	 * @param theta - the angle of the rotation
	 * @return point p - updated with the new coordinates
	 */
	private Point rotate(Point p,double theta){
		int x = p.x;
		int y = p.y;
		double sinT,cosT;
		sinT = Math.sin(theta);
		cosT = Math.cos(theta);
		p.move((int)Math.round(x*cosT + y*sinT), (int)Math.round((-1)*x*sinT + y*cosT));
		return p; 
	}
	
	/**
	 * Draw a rectangular frame.
	 * Draw edges transformed by viewing matrix.
	 * Clip edges to frame if isClipping on.
	 * 
	 */
	public void paint(Graphics g) {
		g.setColor(Color.magenta);
		
		Rectangle rect  = new Rectangle(20 , 20 , resolutionW , resolutionH );	
		
		// go over all edges and paint them
		for (int i = 0;i<edgesCount;i++){
			Edge e = edges.get(i);
			Point p1 = e.getP1();
			Point p2 = e.getP2();
			if (p1 != null && p2 != null) {
				p1 = matrix.multPoint(p1);
				p2 = matrix.multPoint(p2);
				if (isClipping) {
					p1 = new Point(p1.x+(resolutionW+40)/2, p1.y+(resolutionH+60)/2);
					p2 = new Point(p2.x+(resolutionW+40)/2, p2.y+(resolutionH+60)/2);
					Pair<Point> pair = Clipper.ClipLine(p1, p2, rect);
					try {
						if(pair != null){
							p1 = pair.value1;
							p2 = pair.value2;
							
							g.drawLine(p1.x,p1.y,p2.x, p2.y);
						}else{

						}
							
					} catch (Exception e2) {
						System.out.println("View.paint()");
						System.out.println(e2.getMessage());
						e2.printStackTrace();
					}
					
				}else
					g.drawLine(p1.x+(resolutionW+40)/2, p1.y+(resolutionH+60)/2, p2.x+(resolutionW+40)/2, p2.y+(resolutionH+60)/2);
			}
			
		}
		/*}*/
		//print boundaries
		g.setColor(Color.blue);
		g.drawRect(rect.x, rect.y, rect.width, rect.height);
		
		
		
		//draw the 9 parts for debug
		/*
		g.setColor(Color.ORANGE);
		g.drawLine(partXmin, 20, partXmin, this.getHeight() - 20);
		g.drawLine(partXmax, 20, partXmax, this.getHeight() - 20);
		g.drawLine(20, partYmin, this.getWidth() - 20, partYmin);
		g.drawLine(20, partYmax, this.getWidth() - 20, partYmax);
		*/
		
	}
	
	/**
	 * Decide which transformation shall be applied to world.
	 * @param p - mouse clicked point
	 * @return The required operation according to the point 
	 */
	public Operation whichOperation(Point p){
		int x = p.x;
		int y = p.y;
		
		if (x < partXmin || x > partXmax){
			if (y < partYmin || y > partYmax){
				return Operation.ROTATE;
			}
		}
		if (x >= partXmin && x <= partXmax){
			if (y >= partYmin && y <= partYmax){
				return Operation.DRAG;
			}
		}
		return Operation.SCALE;		
	}

	/**
	 * Function for update matrix with scale operation
	 * @param a, b - scale parameters
	 */
	private void scale(double a , double b,double centerX,double centerY){
		matrix.MultScale(a, b,centerX,centerY);
	}
	/**
	 * Function for update matrix with rotate operation
	 * @param theta - the rotation angle 
	 */
	private void rotate(double theta){
		matrix.MultRotation(theta);
	}
	/**
	 * Function for update matrix with translate operation
	 * @param a, b - the translation parameters
	 */
	private void translate(double a , double b){
		matrix.MultTranslate(a, b);
	}
	
	/**
	 * Quit program
	 */
	public static void pressedQ(){
		System.exit(0);
		
	}
	
	/**
	 * change clipping mode
	 */
	public  void pressedC(){
		isClipping = !isClipping;
	}
	
	/**
	 * Set the view to the origin position
	 */
	public void pressedR(){
		matrix = new Matrix(3,3);
		currentOperation = Operation.NONE;
	}
	
	/**
	 * loading new file
	 */
	public void pressedL(){
		try{
			JFileChooser fc = new JFileChooser();
			FileNameExtensionFilter filter = new FileNameExtensionFilter("scn/viw","scn","viw");
		    fc.addChoosableFileFilter(filter);
		    //start browsing from current directory
		    fc.setCurrentDirectory(new File(System.getProperty("user.dir")));
		    fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
			int returnVal = fc.showOpenDialog(null);  
			if (returnVal == JFileChooser.APPROVE_OPTION) {
				String ending[]=fc.getSelectedFile().getName().split("\\.");
				if(ending[1].equals("scn")){
					World world = new World();
					world.readScene(fc.getSelectedFile().getPath());
					setArgs(world);
				}else if(ending[1].equals("viw")){
					readView(fc.getSelectedFile().getPath());
					updateView();
				}else{
					System.out.println("unknown file type");
				}
			}else{
				System.out.println("did not choose a file");
			}
		}catch (Exception e) {
			System.out.println("PROBLEM with file");
		}
	}

	@Override
	/**
	 * Function for handling clicked mouse event
	 */
	public void mouseClicked(MouseEvent arg0) {
		lastPoint = arg0.getPoint();
		//get the operation according to the point coordinates
		currentOperation = whichOperation(lastPoint);
		repaint();
	}

	@Override
	// not in use 
	public void mouseEntered(MouseEvent arg0) {
	}

	@Override
	// not in use
	public void mouseExited(MouseEvent arg0) {
	}

	@Override
	/**
	 * Function for handling Pressed mouse event
	 */
	public void mousePressed(MouseEvent arg0) {
		lastPoint = arg0.getPoint();
		//get the operation according to the point coordinates
		currentOperation = whichOperation(lastPoint);
		repaint();
	}

	@Override
	/**
	 * Function for handling Released mouse event
	 */
	public void mouseReleased(MouseEvent arg0) {
		currentOperation = Operation.NONE;
		this.repaint();
	}

	@Override
	/**
	 * Function for handling Dragged mouse event
	 */
	public void mouseDragged(MouseEvent arg0) {
		Point pEnd = arg0.getPoint();
		//Handling matrix operation according to the different cases
		if (currentOperation == Operation.ROTATE){
			double c = distance(lastPoint, pEnd);
			double b = distance(centerP, lastPoint);
			double a = distance(centerP, pEnd);
			//calculate theta
			double theta = getTheta(a, b, c);
			//rotate according to the point location
			if(lastPoint.x < partXmin){
				if(lastPoint.y < pEnd.y){
					
					rotate(theta);
				}
				else if (lastPoint.y > pEnd.y){
					rotate(-theta);
				}
					
			}else if(lastPoint.x > partXmax){
				if(lastPoint.y < pEnd.y)
					rotate(-theta);
				else if (lastPoint.y > pEnd.y)
					rotate(theta);
			}
			
			
			if (lastPoint.y < partYmin){
				
				if (lastPoint.x < pEnd.x){					
					rotate(-theta);
				}else if(lastPoint.x > pEnd.x){	
					rotate(theta);
				}
			}else if (lastPoint.y >= partYmax){		
				if (lastPoint.x < pEnd.x){
					rotate(theta);
				}else if(lastPoint.x > pEnd.x) {
					rotate(-theta);
				}
			}
			
		}else if (currentOperation == Operation.SCALE){
			double a = distance(lastPoint, centerP);
			double b = distance(pEnd, centerP);
			double c = distance(lastPoint, pEnd);
	
				scale(b/a, b/a,centerX-(resolutionW+40)/2,centerY-(resolutionH+40)/2);
	
		}else if (currentOperation == Operation.DRAG){
			double a = pEnd.x - lastPoint.x ;
			double b = pEnd.y - lastPoint.y ;
			//drag operation an a b direction
			translate(a, b);
		}
		lastPoint = pEnd;
		this.repaint();
	}

	@Override
	//not in use
	public void mouseMoved(MouseEvent arg0) {
		
	}
	/**
	 * Function for calculating distance between two points
	 * @param v1 v2 - points
	 * @return - the distance
	 */
	private double distance(Point v1, Point v2){
		return Math.sqrt(Math.pow(v1.x-v2.x,2)+Math.pow(v1.y-v2.y, 2));
	}
	
	/**
	 * Function for calculating theta according to the cosine theorem 
	 * @param a, b, c
	 * @return theta angle (degrees)
	 */
	private double getTheta(double a , double b, double c){
		double cosC = (Math.pow(a, 2)+Math.pow(b, 2)-Math.pow(c, 2))/(2*a*b);
		return Math.toDegrees(Math.acos(cosC));
	}

	@Override
	/**
	 * Function for handling Pressed Key event
	 */
	public void keyPressed(KeyEvent e) {
		char keyPressed= e.getKeyChar();
		switch (keyPressed){
		case 'R':
		case 'r':	pressedR();
					repaint();
			break;
		case 'C':
		case 'c':	pressedC();
					repaint();
			break;
		case 'Q':
		case 'q': 	pressedQ();
			break;
		case 'L':
		case 'l': 	pressedL();
					repaint();
			break;
		default:
			break;
		}
		
	}
	
	

	@Override
	//not in use
	public void keyReleased(KeyEvent e) {		
	}

	@Override
	//not in use
	public void keyTyped(KeyEvent e) {
	}
	
	
}
